﻿using System;
using System.Collections.Generic;
using Koopakiller.Numerics.FormulaSolver;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Koopakiller.Numerics.Test
{
    [TestClass]
    public class FormulaSolverTest
    {
        [TestMethod(), TestCategory("Koopakiller.Numerics.FormulaSolver")]
        public void TestSolve1()
        {
            //? standard calculation (cosine)
            FormulaCalculator fc = new FormulaCalculator();

            fc.ValueCalculated += fc_ValueCalculated;
            fc.FormulaSplit += fc_FormulaSplit;

            InitFC(fc);

            fc.Values.Add("a", 10);
            fc.Values.Add("b", 10);
            fc.Values.Add("c", 10);
            fc.CreateValueList();

            fc.Calculate();

            Assert.AreEqual(60, (double)fc.Values["α"], 1e-10);
            Assert.AreEqual(60, (double)fc.Values["β"], 1e-10);
            Assert.AreEqual(60, (double)fc.Values["ψ"], 1e-10);
        }
        [TestMethod(), TestCategory("Koopakiller.Numerics.FormulaSolver")]
        public void TestSolve2()
        {
            //? with condition
            FormulaCalculator fc = new FormulaCalculator();

            fc.ValueCalculated += fc_ValueCalculated;
            fc.FormulaSplit += fc_FormulaSplit;

            InitFC(fc);

            fc.Values.Add("c", 4);
            fc.Values.Add("b", 3);
            fc.Values.Add("α", 90);
            fc.CreateValueList();

            fc.Calculate();

            Assert.AreEqual(5, (double)fc.Values["a"], 1e-10);
        }
        [TestMethod(), TestCategory("Koopakiller.Numerics.FormulaSolver")]
        public void TestSolve3()
        {
            //? found second solution?
            FormulaCalculator fc = new FormulaCalculator();

            fc.ValueCalculated += fc_ValueCalculated;
            fc.FormulaSplit += fc_FormulaSplit;

            InitFC(fc);

            fc.Values.Add("a", 3);
            fc.Values.Add("b", 4);
            fc.Values.Add("α", 30);
            fc.CreateValueList();

            fc_FormulaSplit2 = null;

            fc.Calculate();

            Assert.AreNotEqual(null, fc_FormulaSplit2);

            fc_FormulaSplit2.Calculate();
            var x = (double)fc.Values["β"];
            var y = (double)fc_FormulaSplit2.Values["β"];

            //? Complementary angles
            Assert.AreEqual(41.810314895778598066, Math.Min(x, y), 1e-10);
            Assert.AreEqual(180 - 41.810314895778598066, Math.Max(x, y), 1e-10);
        }

        void InitFC(FormulaCalculator fc)
        {
            #region perimeter
            fc.AddFormula(new Formula("u", new string[] { "a", "b", "c" }, v => (double)v["a"] + (double)v["b"] + (double)v["c"]));
            fc.AddFormula(new Formula("u", new string[] { "α", "β", "ψ", "ru" }, v => 8 * (double)v["ru"] * Math.Cos((double)v["α"] / 2) * Math.Cos((double)v["β"] / 2) * Math.Cos((double)v["ψ"] / 2)));
            #endregion
            #region side a
            fc.AddFormula(new Formula("a", new string[] { "b", "c" }, v => Math.Sqrt((double)v["b"] * (double)v["b"] + (double)v["c"] * (double)v["c"])) { CanCalculate = v => v["α"] != null && fc.IsEqual((double)v["α"], 90) });
            fc.AddFormula(new Formula("a", new string[] { "u", "b", "c" }, v => (double)v["u"] - (double)v["a"] - (double)v["c"]));
            fc.AddFormula(new Formula("a", new string[] { "b", "β", "α" }, v => (double)v["b"] / Math.Sin((double)v["β"] * Math.PI / 180D) * Math.Sin((double)v["α"] * Math.PI / 180D)));
            fc.AddFormula(new Formula("a", new string[] { "c", "ψ", "α" }, v => (double)v["c"] / Math.Sin((double)v["ψ"] * Math.PI / 180D) * Math.Sin((double)v["α"] * Math.PI / 180D)));
            fc.AddFormula(new Formula("a", new string[] { "b", "c", "α" }, v => Math.Sqrt((double)v["b"] * (double)v["b"] + (double)v["c"] * (double)v["c"] - 2 * (double)v["b"] * (double)v["c"] * Math.Cos((double)v["α"] * Math.PI / 180D))));
            #endregion
            #region side b
            fc.AddFormula(new Formula("b", new string[] { "u", "a", "c" }, v => (double)v["u"] - (double)v["a"] - (double)v["c"]));
            fc.AddFormula(new Formula("b", new string[] { "a", "α", "β" }, v => (double)v["a"] / Math.Sin((double)v["α"] * Math.PI / 180D) * Math.Sin((double)v["β"] * Math.PI / 180D)));
            fc.AddFormula(new Formula("b", new string[] { "c", "ψ", "β" }, v => (double)v["c"] / Math.Sin((double)v["ψ"] * Math.PI / 180D) * Math.Sin((double)v["β"] * Math.PI / 180D)));
            fc.AddFormula(new Formula("b", new string[] { "a", "c", "β" }, v => Math.Sqrt((double)v["a"] * (double)v["a"] + (double)v["c"] * (double)v["c"] - 2 * (double)v["a"] * (double)v["c"] * Math.Cos((double)v["β"] * Math.PI / 180D))));
            #endregion
            #region side c
            fc.AddFormula(new Formula("c", new string[] { "u", "a", "b" }, v => (double)v["u"] - (double)v["a"] - (double)v["b"]));
            fc.AddFormula(new Formula("c", new string[] { "a", "α", "ψ" }, v => (double)v["b"] / Math.Sin((double)v["α"] * Math.PI / 180D) * Math.Sin((double)v["ψ"] * Math.PI / 180D)));
            fc.AddFormula(new Formula("c", new string[] { "b", "β", "ψ" }, v => (double)v["c"] / Math.Sin((double)v["β"] * Math.PI / 180D) * Math.Sin((double)v["ψ"] * Math.PI / 180D)));
            fc.AddFormula(new Formula("c", new string[] { "a", "b", "ψ" }, v => Math.Sqrt((double)v["a"] * (double)v["a"] + (double)v["b"] * (double)v["b"] - 2 * (double)v["a"] * (double)v["b"] * Math.Cos((double)v["ψ"] * Math.PI / 180D))));
            #endregion
            #region angle α
            fc.AddFormula(new Formula("α", new string[] { "β", "ψ" }, v => 180D - (double)v["β"] - (double)v["ψ"]));
            fc.AddFormula(new Formula("α", new string[] { "a", "b", "c" }, v => Math.Acos(((double)v["b"] * (double)v["b"] + (double)v["c"] * (double)v["c"] - (double)v["a"] * (double)v["a"]) / (2 * (double)v["b"] * (double)v["c"])) / Math.PI * 180D));
            fc.AddFormula(new Formula("α", new string[] { "β", "b", "a" }, v => Sin(fc, "b", "a", "β", "α", v)));
            fc.AddFormula(new Formula("α", new string[] { "ψ", "c", "a" }, v => Sin(fc, "c", "a", "ψ", "α", v)));
            #endregion
            #region angle β
            fc.AddFormula(new Formula("β", new string[] { "α", "ψ" }, v => 180D - (double)v["α"] - (double)v["ψ"]));
            fc.AddFormula(new Formula("β", new string[] { "a", "b", "c" }, v => Math.Acos(((double)v["a"] * (double)v["a"] + (double)v["c"] * (double)v["c"] - (double)v["b"] * (double)v["b"]) / (2 * (double)v["a"] * (double)v["c"])) / Math.PI * 180D));
            fc.AddFormula(new Formula("β", new string[] { "α", "a", "b" }, v => Sin(fc, "a", "b", "α", "β", v)));
            fc.AddFormula(new Formula("β", new string[] { "ψ", "c", "b" }, v => Sin(fc, "c", "b", "ψ", "β", v)));
            #endregion
            #region angle ψ
            fc.AddFormula(new Formula("ψ", new string[] { "α", "β" }, v => 180D - (double)v["α"] - (double)v["β"]));
            fc.AddFormula(new Formula("ψ", new string[] { "a", "b", "c" }, v => Math.Acos(((double)v["a"] * (double)v["a"] + (double)v["b"] * (double)v["b"] - (double)v["c"] * (double)v["c"]) / (2 * (double)v["a"] * (double)v["b"])) / Math.PI * 180D));
            fc.AddFormula(new Formula("ψ", new string[] { "β", "b", "c" }, v => Sin(fc, "b", "c", "β", "ψ", v)));
            fc.AddFormula(new Formula("ψ", new string[] { "α", "a", "c" }, v => Sin(fc, "a", "c", "α", "ψ", v)));
            #endregion
            #region altitudes a
            fc.AddFormula(new Formula("ha", new string[] { "β", "c" }, v => Math.Sin((double)v["β"] / 180D * Math.PI) * (double)v["c"]));
            fc.AddFormula(new Formula("ha", new string[] { "ψ", "b" }, v => Math.Sin((double)v["ψ"] / 180D * Math.PI) * (double)v["b"]));
            fc.AddFormula(new Formula("ha", new string[] { "A", "a" }, v => (2 * (double)v["A"]) / (double)v["a"]));
            #endregion
            #region altitudes b
            fc.AddFormula(new Formula("hb", new string[] { "α", "c" }, v => Math.Sin((double)v["α"] / 180D * Math.PI) * (double)v["c"]));
            fc.AddFormula(new Formula("hb", new string[] { "ψ", "a" }, v => Math.Sin((double)v["ψ"] / 180D * Math.PI) * (double)v["a"]));
            fc.AddFormula(new Formula("hb", new string[] { "A", "b" }, v => (2 * (double)v["A"]) / (double)v["b"]));
            #endregion
            #region altitudes c
            fc.AddFormula(new Formula("hc", new string[] { "α", "b" }, v => Math.Sin((double)v["α"] / 180D * Math.PI) * (double)v["b"]));
            fc.AddFormula(new Formula("hc", new string[] { "β", "a" }, v => Math.Sin((double)v["β"] / 180D * Math.PI) * (double)v["a"]));
            fc.AddFormula(new Formula("hc", new string[] { "A", "c" }, v => (2 * (double)v["A"]) / (double)v["c"]));
            #endregion
            #region area
            fc.AddFormula(new Formula("A", new string[] { "a", "ha" }, v => (double)v["a"] * (double)v["ha"] / 2));
            fc.AddFormula(new Formula("A", new string[] { "b", "hb" }, v => (double)v["b"] * (double)v["hb"] / 2));
            fc.AddFormula(new Formula("A", new string[] { "c", "hc" }, v => (double)v["c"] * (double)v["hc"] / 2));
            #endregion
        }

        //? help function for not unique sine
        static IEnumerable<Dictionary<string, double?>> Sin(FormulaCalculator fs, string sideName, string targetSideName, string angleName, string targetAngleName, Dictionary<string, double?> dic)
        {
            var sin = Math.Sin((double)dic[angleName] / 180D * Math.PI);
            dic[targetAngleName] = Math.Asin(sin / (double)dic[sideName] * (double)dic[targetSideName]) / Math.PI * 180D;
            if (!fs.IsEqual((double)dic[angleName], 90))
            {
                //? not unique because φ != 90°
                var dic2 = dic.Copy(x => x, y => y);
                dic2[targetAngleName] = 180 - dic[targetAngleName];

                yield return dic2;// return second dictionary
            }
            yield return dic;//contains origin values
        }

        private void fc_FormulaSplit(object sender, FormulaSplitEventArgs e)
        {
            fc_FormulaSplit2 = e.NewValues;
            //throw new NotImplementedException();
        }

        private void fc_ValueCalculated(object sender, FormulaCalculatedEventArgs e)
        {
            //throw new NotImplementedException();
        }

        FormulaCalculator fc_FormulaSplit2 = null;
    }
}
